package ar.com.temporis.framework.common.comparator;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import ar.com.temporis.framework.common.comparator.logger.ComparatorLogger;

/**
 * Default implementation for {@code BeanComparator} that compare two beans of
 * the same class and logs his differences.
 * 
 * @author raul.bajales@gmail.com
 */
public class DefaultBeanComparator extends AbstractBeanComparator {

  public DefaultBeanComparator(ComparatorLogger logger) {
    super(logger);
  }

  @SuppressWarnings("unchecked")
  private static Map<Class, String> verifierMapping =
      new HashMap<Class, String>();

  /**
   * Default method name that handles the objects that are not from the business
   * model.
   */
  private static final String DEFAULT_VERIFIER_METHOD = "defaultVerifier";

  static {
    verifierMapping.put(List.class, "listVerifier");
    verifierMapping.put(Map.class, "mapVerifier");
  }

  /**
   * Calls to the recursive methods to perform the comparison.
   * 
   * @param bean1 The first bean.
   * @param bean2 The second bean.
   */
  @Override
  protected void doComparison(Object bean1, Object bean2) {
    if (!checkForNull(bean1, bean2, null)) {
      doVerificationOnFields(bean1, bean2, null);
    }
  }

  /**
   * Recursive method that performs the verification and calls the comparator
   * methods.
   * 
   * @param object1 The first object.
   * @param object2 The second object.
   * @param path The path to the object, only if the object represents a field.
   */
  private void doVerificationOnFields(Object object1, Object object2,
      String path) {
    path = (path != null ? (path + ".") : "");
    for (Field field : object1.getClass().getDeclaredFields()) {
      try {
        String methodName = verifierMapping.get(field.getType());
        Object fieldValue1 = extractFieldValue(field, object1);
        Object fieldValue2 = extractFieldValue(field, object2);
        String fieldName = path + field.getName();
        if (checkForNull(fieldValue1, fieldValue2, fieldName)) {
          break;
        }
        if (methodName == null && fieldCouldBeModelObject(field)) {
          doVerificationOnFields(fieldValue1, fieldValue2, fieldName);
        } else {
          if (methodName == null && (!fieldCouldBeModelObject(field))) {
            methodName = DEFAULT_VERIFIER_METHOD;
          }
          invokeComparatorMethod(methodName, fieldValue1, fieldValue2, fieldName);
        }
      } catch (InvokeComparatorMethodException e) {
        logException(e);
        return;
      } catch (ExtractFieldValueException e) {
        // nothing to do, just try with the next field.
        break;
      }
    }
  }

  /**
   * Logs an exception during the comparison process.
   * 
   * @param e The exception to log.
   */
  private void logException(Exception e) {
    StringWriter writer = new StringWriter();
    e.printStackTrace(new PrintWriter(writer));
    addToLog("Cannot compare!, see stackTrace...");
    addToLog(writer.toString());
    setResult(false);
  }

  /**
   * Invokes a comparator method.
   * 
   * @param methodName The name of the method.
   * @param fieldFromBean1 The field from the first bean.
   * @param fieldFromBean2 The field from the second bean.
   * @param path The path to the field.
   * @throws InvokeComparatorMethodException If the method cannot be invoked.
   */
  private void invokeComparatorMethod(String methodName, Object fieldFromBean1,
      Object fieldFromBean2, String path)
      throws InvokeComparatorMethodException {
    try {
      this.getClass().getMethod(methodName,
          new Class[] {Object.class, Object.class, String.class}).invoke(this,
          fieldFromBean1, fieldFromBean2, path);
    } catch (IllegalAccessException e) {
      throw new InvokeComparatorMethodException(e);
    } catch (InvocationTargetException e) {
      throw new InvokeComparatorMethodException(e);
    } catch (NoSuchMethodException e) {
      throw new InvokeComparatorMethodException(e);
    }
  }

  /**
   * Tests if a field type could be a business model object.
   * 
   * @param field The field to be tested,
   * @return True if the field type is different from the regular java types
   *         that can be found in a business model object. By regular java types
   *         I mean types like Integer, Double, String, Map, List, those types
   *         that you can say that are not business model objects, and you do
   *         not to dig into to search for more differences, only apply the
   *         related "equals" method.
   */
  @SuppressWarnings("unchecked")
  private boolean fieldCouldBeModelObject(Field field) {
    List<Class> classes = new ArrayList<Class>();
    classes.add(Integer.class);
    classes.add(String.class);
    classes.add(Double.class);
    classes.add(Collection.class);
    classes.add(Map.class);
    return !classes.contains(field.getType());
  }

  /**
   * Check two object are null (or at least one of them).
   * 
   * @param fieldFromBean1 The first object.
   * @param fieldFromBean2 The second object.
   * @param path The path to the field.
   * @return True if both objects are null, false otherwise.
   */
  private boolean checkForNull(Object fieldFromBean1, Object fieldFromBean2,
      String path) {
    if (fieldFromBean1 == null || fieldFromBean2 == null) {
      if (!(fieldFromBean1 == null && fieldFromBean2 == null)) {
        logDifference(path, fieldFromBean1, fieldFromBean2);
      }
      return true;
    }
    return false;
  }

  /**
   * Check two List for empty or different size.
   * 
   * @param fieldFromBean1 The first list.
   * @param fieldFromBean2 The second list.
   * @param path The path to the field.
   * @return True if there is any difference in size or if one of both (but no
   *         both) lists is empty.
   */
  @SuppressWarnings("unchecked")
  private boolean checkForEmptyOrDifferentSize(Object fieldFromBean1,
      Object fieldFromBean2, String path) {
    List list1 = (List) fieldFromBean1;
    List list2 = (List) fieldFromBean2;
    if (list1.isEmpty() || list2.isEmpty() || (list1.size() != list2.size())) {
      if (!(list1.isEmpty() && list2.isEmpty())) {
        logDifference(path, fieldFromBean1, fieldFromBean2);
      }
      return true;
    }
    return false;
  }

  /**
   * Shows the difference between two fields.
   * 
   * @param fieldName The name of the field.
   * @param fieldFromBean1 The field from the first bean.
   * @param fieldFromBean2 The field from the second bean.
   */
  @SuppressWarnings("unchecked")
  private void logDifference(String fieldName, Object fieldFromBean1,
      Object fieldFromBean2) {
    setResult(false);
    addToLog("\nDifferent data found in the field '" + fieldName + "':");
    addToLog("value in Object 1: " + fieldFromBean1);
    addToLog("value in Object 2: " + fieldFromBean2);
    if (fieldFromBean1 instanceof List && fieldFromBean2 instanceof List) {
      addToLog("size in Object 1: " + ((List) fieldFromBean1).size());
      addToLog("size in Object 2: " + ((List) fieldFromBean2).size());
    }
  }

  /**
   * Default verification. relies on the "equals" implementation of both beans.
   * Should be called when both beans are not business model objects.
   * 
   * @param field1 The first object.
   * @param field2 The second object.
   * @param path The path to this field.
   */
  public void defaultVerifier(Object field1, Object field2, String path) {
    if (!field1.equals(field2)) {
      logDifference(path, field1, field2);
    }
  }

  /**
   * Verify a list field (should implement {@code List}) on two beans.
   * 
   * @param field1 The first list.
   * @param field2 The second list.
   * @param path The path to this list.
   */
  @SuppressWarnings("unchecked")
  public void listVerifier(Object field1, Object field2, String path) {
    if (!checkForEmptyOrDifferentSize(field1, field2, path)) {
      List list1 = (List) field1;
      List list2 = (List) field2;
      for (int index = 0; index < list1.size(); index++) {
        String fieldName = path + "[" + index + "]";
        doVerificationOnFields(list1.get(index), list2.get(index), fieldName);
      }
    }
  }

  /**
   * Verify a map field (should implement {@Map}) on two beans.
   * 
   * @param field1 The first map.
   * @param field2 The second map.
   * @param path The path to this map.
   */
  public void mapVerifier(Object field1, Object field2, String path) {
    throw new RuntimeException("Map verifier not implemented yet!");
  }
}
